/**
 * 
 */
package org.jenkon.core;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.jenkon.commons.io.utils.FilenameUtils;
import org.jenkon.commons.lang.filter.Filter;
import org.jenkon.commons.lang.utils.ClassUtils;
import org.jenkon.commons.lang.utils.StringUtils;
import org.jenkon.commons.lang.utils.SystemUtils;
import org.jenkon.core.annotation.Bean;
import org.jenkon.core.filter.JarPackageNameFilter;
import org.junit.Test;

/**
 * @author Jens Konopka
 * 
 */
public class AnnotatedBeanScanner {

	private static final int CLASS_EXTENSION_LENGTH = 6;

	@Test
	public void test() throws Exception {
		scan(new DefaultApplicationContext(), new String[] { "org.jenkon", "org.easymock" });
	}

	public void scan(final ApplicationContext applicationContext, final String[] packageNames) throws Exception {

		applicationContext.addBean("applicationContext", applicationContext);

		for (final Class<?> clz : getBeans(packageNames)) {
			final Bean beanName = clz.getAnnotation(Bean.class);
			if (StringUtils.isEmpty(beanName.value())) {
				applicationContext.addBean(ClassUtils.newInstance(clz));
			} else {
				applicationContext.addBean(beanName.value(), ClassUtils.newInstance(clz));
			}
		}

	}

	/**
	 * @param packageNames
	 * @return
	 * @throws ZipException
	 * @throws IOException
	 */
	private List<Class<?>> getBeans(final String[] packageNames) throws ZipException, IOException {

		final List<Class<?>> classes = new ArrayList<Class<?>>();
		for (final String packageName : packageNames) {
			classes.addAll(getBeansByPackageName(packageName));
		}
		return classes;

	}

	private List<Class<?>> getBeansByPackageName(final String packageName) throws ZipException, IOException {

		final List<Class<?>> classes = new ArrayList<Class<?>>();

		final List<File> files = SystemUtils.getJavaClassPathFiles();
		for (final File file : files) {

			if (FilenameUtils.endsWith(file, "jar")) {
				classes.addAll(getBeansByJar(packageName, new ZipFile(file)));
			} else {

				final String fileName = packageName.replace(".", SystemUtils.getFileSeparator());
				final File directory = new File(FilenameUtils.concat(file.getPath(), fileName));

				if (directory.isDirectory()) {
					classes.addAll(getClasses(true, directory, packageName));
				}

			}

		}

		return classes;

	}

	/**
	 * @param packageName
	 * @param zipFile
	 * @return
	 */
	private List<Class<?>> getBeansByJar(final String packageName, final ZipFile zipFile) {

		final List<Class<?>> classes = new ArrayList<Class<?>>();

		final List<Filter<ZipEntry>> filter = new ArrayList<Filter<ZipEntry>>();
		filter.add(new JarPackageNameFilter(packageName));
		final JarClassIterator iterator = new JarClassIterator(zipFile, filter);
		while (iterator.hasNext()) {
			final Class<?> clz = iterator.next();
			if (ClassUtils.hasAnnotation(clz, Bean.class)) {
				classes.add(clz);
			}
		}

		return classes;
	}

	/**
	 * @param file the {@link File} to create the {@link Class}
	 * @param packageName the name of the package from {@link Class}
	 * @return the created {@link Class}
	 * @throws ClassNotFoundException if the {@link Class} was not found
	 */
	private Class<?> getClass(final File file, final String packageName) throws ClassNotFoundException {
		final StringBuilder sb = new StringBuilder();
		sb.append(packageName);
		sb.append(".");
		sb.append(getClassname(file));
		return Class.forName(sb.toString());
	}

	/**
	 * @param file
	 * @return
	 */
	private String getClassname(final File file) {
		return file.getName().substring(0, file.getName().length() - CLASS_EXTENSION_LENGTH);
	}

	/**
	 * @param file the {@link File}
	 * @param packageName the name of the package
	 * @return the created name of the package from parameter {@link File} and packageName
	 */
	private String changePackageName(final File file, final String packageName) {
		return (packageName + "." + file.getName()).replaceAll("^\\.", "");
	}

	/**
	 * Check the {@link File} is this a {@link Class} or not.
	 * 
	 * @param file {@link File} to check of is class
	 * @return <code>true</code> if the {@link File} is a class, or <code>false</code> if the {@link File} is not a class.
	 */
	private boolean isClass(final File file) {
		return file.getName().toLowerCase().endsWith(".class");
	}

	private List<Class<?>> getClasses(final boolean first, final File file, String packageName) {

		final List<Class<?>> classes = new ArrayList<Class<?>>();

		if (file.isDirectory()) {

			if (!first) {
				packageName = changePackageName(file, packageName);
			}

			for (final String subFileOrDir : file.list()) {
				classes.addAll(getClasses(false, new File(file, subFileOrDir), packageName));
			}

		} else if (isClass(file)) {

			try {

				final Class<?> clz = getClass(file, packageName);
				if (ClassUtils.hasAnnotation(clz, Bean.class)) {
					classes.add(clz);
				}

			} catch (Throwable ex) {
			}

		}

		return classes;
	}

}
